diff options
Diffstat (limited to 'app/[lng]/evcp/(evcp)/bid/page.tsx')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/bid/page.tsx | 111 |
1 files changed, 111 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/bid/page.tsx b/app/[lng]/evcp/(evcp)/bid/page.tsx new file mode 100644 index 00000000..7480ce88 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/bid/page.tsx @@ -0,0 +1,111 @@ +import { Suspense } from "react" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { + getBiddings, + getBiddingStatusCounts, + getBiddingTypeCounts, + getBiddingManagerCounts, + getBiddingMonthlyStats +} from "@/lib/bidding/service" +import { searchParamsCache } from "@/lib/bidding/validation" +import { BiddingsPageHeader } from "@/lib/bidding/list/biddings-page-header" +import { BiddingsStatsCards } from "@/lib/bidding/list/biddings-stats-cards" +import { BiddingsTable } from "@/lib/bidding/list/biddings-table" + +export const metadata = { + title: "입찰 목록", + description: "입찰 공고를 생성하고 진행 상황을 관리할 수 있습니다.", +} + +interface BiddingsPageProps { + searchParams: Record<string, string | string[] | undefined> +} + +export default async function BiddingsPage({ searchParams }: BiddingsPageProps) { + // ✅ nuqs searchParamsCache로 파싱 (타입 안전성 보장) + const search = searchParamsCache.parse(searchParams) + + // ✅ 모든 데이터를 병렬로 로드 + const promises = Promise.all([ + getBiddings(search), + getBiddingStatusCounts(), + getBiddingTypeCounts(), + getBiddingManagerCounts(), + getBiddingMonthlyStats(), + ]) + + return ( + <Shell className="gap-4"> + {/* ═══════════════════════════════════════════════════════════════ */} + {/* 페이지 헤더 */} + {/* ═══════════════════════════════════════════════════════════════ */} + <BiddingsPageHeader /> + + {/* ═══════════════════════════════════════════════════════════════ */} + {/* 통계 카드들 */} + {/* ═══════════════════════════════════════════════════════════════ */} + <Suspense fallback={<BiddingsStatsCardsSkeleton />}> + <BiddingsStatsCardsWrapper promises={promises} /> + </Suspense> + + {/* ═══════════════════════════════════════════════════════════════ */} + {/* 메인 테이블 */} + {/* ═══════════════════════════════════════════════════════════════ */} + <Suspense + fallback={ + <DataTableSkeleton + columnCount={20} + searchableColumnCount={3} + filterableColumnCount={4} + cellWidths={["10rem", "8rem", "12rem", "15rem", "10rem", "8rem"]} + shrinkZero + /> + } + > + <BiddingsTable promises={promises} /> + </Suspense> + </Shell> + ) +} + +// ═══════════════════════════════════════════════════════════════ +// 통계 카드 래퍼 컴포넌트 +// ═══════════════════════════════════════════════════════════════ +async function BiddingsStatsCardsWrapper({ + promises +}: { + promises: Promise<[ + Awaited<ReturnType<typeof getBiddings>>, + Awaited<ReturnType<typeof getBiddingStatusCounts>>, + Awaited<ReturnType<typeof getBiddingTypeCounts>>, + Awaited<ReturnType<typeof getBiddingManagerCounts>>, + Awaited<ReturnType<typeof getBiddingMonthlyStats>> + ]> +}) { + const [biddingsResult, statusCounts, typeCounts, managerCounts, monthlyStats] = await promises + + return ( + <BiddingsStatsCards + total={biddingsResult.total} + statusCounts={statusCounts} + typeCounts={typeCounts} + managerCounts={managerCounts} + monthlyStats={monthlyStats} + /> + ) +} + +// 통계 카드 스켈레톤 +function BiddingsStatsCardsSkeleton() { + return ( + <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> + {Array.from({ length: 4 }).map((_, i) => ( + <div key={i} className="rounded-lg border p-6"> + <div className="h-4 bg-muted rounded animate-pulse mb-2" /> + <div className="h-8 bg-muted rounded animate-pulse" /> + </div> + ))} + </div> + ) +}
\ No newline at end of file |
